﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Benchmark.Db.Base.DataObjects;
using Benchmark.Db.Base.DataCreation;
using System.Collections;
using System.Threading.Tasks;
using System.Reflection;

namespace Benchmark.Db.Base
{
    public class TestManager<T>
        where T : class, IDataObject, new()
    {
        private IDbBenchmarkConnectionV1 CurrentConnection { get; set; }
        private T[] m_samples;
        private static int DefaultSampleSize = 100000;

        public event ProcessDelegate Process;

        public TestManager(IDbBenchmarkConnectionV1 connection)
        {
            this.CurrentConnection = connection;
        }

        protected void Initialize()
        {
            this.DoProcess(0, string.Format("Initializing database ({0})", this.CurrentConnection.Name));
            this.CurrentConnection.Initialize<Record1>();

            this.DoProcess(0, "Cleaning database");
            this.CurrentConnection.ClearDB();

            this.DoProcess(0, "Check indexes");
            this.CurrentConnection.EnsureIndexes<Record1>();
            //    (typeof(T), "Key1");
            //this.CurrentConnection.EnsureIndex(typeof(T), "Key2");
            //this.CurrentConnection.EnsureIndex(typeof(T), "IntValue1");
            //this.CurrentConnection.EnsureIndex(typeof(T), "TextValue1");
            //this.CurrentConnection.EnsureIndex(typeof(T), "TimeValue1");
            this.DoProcess(0, "Connection initialized");
        }

        protected void DoProcess(int percent, string action)
        {
            if (this.Process != null)
                this.Process(this, percent, action);
        }

        protected void WriteTestStep(Step step)
        {
            throw new NotImplementedException();
        }

        public Step TestInsert()
        {
            Step testStep = new Step();
            testStep.Description = "Insert";
            testStep.RecordCount = TestManager<T>.DefaultSampleSize;

            RandomDataCreator<T> dataCreator = new RandomDataCreator<T>();

            this.DoProcess(0, string.Format("Creating {0:#,#} sample record(s)", testStep.RecordCount));
            this.m_samples = dataCreator.Create(testStep.RecordCount);

            this.DoProcess(0, "Inserting");
            testStep.Start = DateTime.Now;

            this.CurrentConnection.Insert(this.m_samples);
            testStep.End = DateTime.Now;
            testStep.Duration = testStep.End - testStep.Start;
            testStep.Status = TestStepStatus.Completed;

            this.DoProcess(0, "Completed");
            this.WriteTestStep(testStep);
            return testStep;
        }

        public Step TestUpdate()
        {
            Step testStep = new Step();
            testStep.Description = "Update";
            testStep.RecordCount = TestManager<T>.DefaultSampleSize;

            RandomDataCreator<T> dataCreator = new RandomDataCreator<T>();

            this.DoProcess(0, string.Format("Updating {0:#,#} sample record(s)", testStep.RecordCount));
            this.m_samples = dataCreator.ChangeValues(this.m_samples);

            this.DoProcess(0, "Updating");
            testStep.Start = DateTime.Now;

            this.CurrentConnection.Update("Key1", this.m_samples);
            testStep.End = DateTime.Now;
            testStep.Duration = testStep.End - testStep.Start;
            testStep.Status = TestStepStatus.Completed;

            this.DoProcess(0, "Completed");
            this.WriteTestStep(testStep);
            return testStep;
        }

        protected Step TestLoad(PropertyInfo used, bool multiThreaded)
        {
            Step testStep = new Step();
            testStep.Description = string.Format("Load test for '{0}' column. Type: {1}", used.Name, used.PropertyType.Name);

            testStep.RecordCount = TestManager<T>.DefaultSampleSize / 10;

            RandomDataCreator<T> dataCreator = new RandomDataCreator<T>();

            this.DoProcess(0, string.Format("Loading {0:#,#} sample record(s) with key '{0}'", testStep.RecordCount, used.Name));
            List<object> keyList = new List<object>();

            int start = this.m_samples.Length / 5;
            int end = start + testStep.RecordCount;
            for (int i = start; i < end; i++)
                keyList.Add(this.m_samples[i].PrimaryKey); //WARN: used?

            if (multiThreaded)
            {
                List<object[]> queue = new List<object[]>();
                for (int i = 0; i < this.m_samples.Length; i = i + 100)
                {
                    object[] packet = new object[100];
                    keyList.CopyTo(i, packet, 0, 100);
                    queue.Add(packet);
                }

                this.DoProcess(0, "Loading");
                testStep.Start = DateTime.Now;

                ParallelOptions options = new ParallelOptions();
                options.MaxDegreeOfParallelism = 10;

                Parallel.ForEach(queue, options, packet =>
                {
                    this.CurrentConnection.Load<T, object>(used.Name, packet);
                });
            }
            else
            {
                object[] keyArray = keyList.ToArray();

                this.DoProcess(0, "Loading");
                testStep.Start = DateTime.Now;
                this.CurrentConnection.Load<T, object>(used.Name, keyArray);
            }

            testStep.End = DateTime.Now;
            testStep.Duration = testStep.End - testStep.Start;
            testStep.Status = TestStepStatus.Completed;

            this.DoProcess(0, "Completed");
            this.WriteTestStep(testStep);
            return testStep;
        }

        public Step[] TestLoad(bool multiThreaded)
        {
            Type type = typeof(T);
            List<Step> steps = new List<Step>();
            PropertyInfo[] props = AttributeHelper<UseForLoadTestAttribute, T>.GetDefinedProperties();

            if (multiThreaded)
            {
                Parallel.ForEach<PropertyInfo>(props, prop =>
                {
                    Step step = this.TestLoad(prop, true);
                    steps.Add(step);
                });
            }
            else
                foreach (PropertyInfo prop in props)
                {
                    Step step = this.TestLoad(prop, false);
                    steps.Add(step);
                }

            return steps.ToArray();
        }

        public Step TestDelete()
        {
            Step testStep = new Step();
            testStep.Description = "Delete";
            testStep.RecordCount = 10000;

            RandomDataCreator<T> dataCreator = new RandomDataCreator<T>();

            this.DoProcess(0, string.Format("Deleting {0:#,#} record(s)", testStep.RecordCount));

            List<Guid> guidList = new List<Guid>();

            for (int i = this.m_samples.Length; i > this.m_samples.Length - 10000; i--)
                guidList.Add(this.m_samples[i].PrimaryKey);

            this.DoProcess(0, "Deleting");

            testStep.Start = DateTime.Now;

            Guid[] guidArray = guidList.ToArray();

            this.CurrentConnection.Delete<T, Guid>(guidArray);

            testStep.End = DateTime.Now;
            testStep.Duration = testStep.End - testStep.Start;
            testStep.Status = TestStepStatus.Completed;

            this.DoProcess(0, "Completed");
            this.WriteTestStep(testStep);
            return testStep;
        }

        public Step TestInsertWithTransaction()
        {
            Step testStep = new Step();
            testStep.Description = "Insert with transaction";
            testStep.RecordCount = TestManager<T>.DefaultSampleSize;

            RandomDataCreator<T> dataCreator = new RandomDataCreator<T>();

            this.DoProcess(0, string.Format("Creating {0:#,#} sample record(s) with using transaction (per 1000 record)", testStep.RecordCount));
            this.m_samples = dataCreator.Create(testStep.RecordCount);

            this.DoProcess(0, "Inserting");
            testStep.Start = DateTime.Now;

            for (int i = 0; i < 100; i++)
            {
                T[] packet = new T[1000];
                Array.Copy(this.m_samples, i * 1000, packet, 0, 1000);
                this.CurrentConnection.BeginTransaction();
                this.CurrentConnection.Insert(packet);
                this.CurrentConnection.CommitTransaction();
            }

            testStep.End = DateTime.Now;
            testStep.Duration = testStep.End - testStep.Start;
            testStep.Status = TestStepStatus.Completed;

            this.DoProcess(0, "Completed");
            this.WriteTestStep(testStep);
            return testStep;
        }

        public Step TestUpdateWithTransaction()
        {
            Step testStep = new Step();
            testStep.Description = "Update with transaction";
            testStep.RecordCount = TestManager<T>.DefaultSampleSize;

            RandomDataCreator<T> dataCreator = new RandomDataCreator<T>();

            this.DoProcess(0, string.Format("Updating {0:#,#} sample record(s) with using transaction (per 1000 record)", testStep.RecordCount));
            this.m_samples = dataCreator.ChangeValues(this.m_samples);

            this.DoProcess(0, "Updating");
            testStep.Start = DateTime.Now;

            for (int i = 0; i < 100; i++)
            {
                T[] packet = new T[1000];
                Array.Copy(this.m_samples, i * 1000, packet, 0, 1000);
                this.CurrentConnection.BeginTransaction();
                this.CurrentConnection.Update("Key1", packet);
                this.CurrentConnection.CommitTransaction();
            }

            testStep.End = DateTime.Now;
            testStep.Duration = testStep.End - testStep.Start;
            testStep.Status = TestStepStatus.Completed;

            this.DoProcess(0, "Completed");
            this.WriteTestStep(testStep);
            return testStep;
        }

        public Step TestRollback()
        {
            Step testStep = new Step();
            testStep.Description = "Update";
            testStep.RecordCount = 15000;

            RandomDataCreator<T> dataCreator = new RandomDataCreator<T>();

            this.DoProcess(0, "Rollback transaction after update 10.000 sample record(s) and insert 5.000 record");

            T[] packet1 = new T[10000];
            T[] packet2 = new T[10000];

            Array.Copy(this.m_samples, packet1, 10000);
            packet1 = dataCreator.ChangeValues(packet1);
            packet2 = dataCreator.Create(5000);

            testStep.Start = DateTime.Now;

            this.CurrentConnection.BeginTransaction();
            this.DoProcess(0, "Update");
            this.CurrentConnection.Update("Key1", packet1);
            this.DoProcess(0, "Insert");
            this.CurrentConnection.Insert(packet2);
            this.CurrentConnection.RollbackTransaction();


            testStep.End = DateTime.Now;
            testStep.Duration = testStep.End - testStep.Start;
            testStep.Status = TestStepStatus.Completed;

            this.DoProcess(0, "Completed");
            this.WriteTestStep(testStep);
            return testStep;
        }

        public Step TestBulkInsert()
        {
            Step testStep = new Step();
            testStep.Description = "Bulk insert 100.000 record(per 10.000 record)";
            testStep.RecordCount = 100000;

            RandomDataCreator<T> dataCreator = new RandomDataCreator<T>();

            this.DoProcess(0, "Creating 100.000 sample record(s)");
            this.m_samples = dataCreator.Create(testStep.RecordCount);

            this.DoProcess(0, "Inserting");
            testStep.Start = DateTime.Now;

            for (int i = 0; i < 10; i++)
            {
                T[] packet = new T[10000];
                Array.Copy(this.m_samples, i * 10000, packet, 0, 10000);

                this.CurrentConnection.BulkInsert(packet);
            }

            testStep.End = DateTime.Now;
            testStep.Duration = testStep.End - testStep.Start;
            testStep.Status = TestStepStatus.Completed;

            this.DoProcess(0, "Completed");
            this.WriteTestStep(testStep);
            return testStep;
        }

        public Step TestMultithreadedInsert()
        {
            Step testStep = new Step();
            testStep.Description = "Multithreaded Insert";
            testStep.RecordCount = TestManager<T>.DefaultSampleSize * 10;

            RandomDataCreator<T> dataCreator = new RandomDataCreator<T>();

            this.DoProcess(0, string.Format("Creating {0:#,#} sample record(s)", testStep.RecordCount));
            this.m_samples = dataCreator.Create(testStep.RecordCount);

            this.DoProcess(0, "Inserting");
            testStep.Start = DateTime.Now;

            List<T[]> queue = new List<T[]>();
            for (int i = 0; i < this.m_samples.Length; i = i + 100)
            {
                T[] packet = new T[100];
                Array.Copy(this.m_samples, i, packet, 0, 100);
                queue.Add(packet);
            }

            ParallelOptions options = new ParallelOptions();
            options.MaxDegreeOfParallelism = 10;

            Parallel.ForEach(queue, options, packet =>
            {
                this.CurrentConnection.Insert(packet);
            });

            testStep.End = DateTime.Now;
            testStep.Duration = testStep.End - testStep.Start;
            testStep.Status = TestStepStatus.Completed;

            this.DoProcess(0, "Completed");
            this.WriteTestStep(testStep);
            return testStep;
        }

        public Step TestMultithreadedUpdate()
        {
            Step testStep = new Step();
            testStep.Description = "Multithreaded Update";
            testStep.RecordCount = TestManager<T>.DefaultSampleSize * 10;

            RandomDataCreator<T> dataCreator = new RandomDataCreator<T>();

            this.DoProcess(0, string.Format("Updating {0:#,#} sample record(s)", testStep.RecordCount));
            this.m_samples = dataCreator.ChangeValues(this.m_samples);

            this.DoProcess(0, "Updating");
            testStep.Start = DateTime.Now;

            List<T[]> queue = new List<T[]>();
            for (int i = 0; i < this.m_samples.Length; i = i + 100)
            {
                T[] packet = new T[100];
                Array.Copy(this.m_samples, i, packet, 0, 100);
                queue.Add(packet);
            }

            ParallelOptions options = new ParallelOptions();
            options.MaxDegreeOfParallelism = 10;

            Parallel.ForEach(queue, options, packet =>
            {
                this.CurrentConnection.Update("Key1", packet);
            });

            testStep.End = DateTime.Now;
            testStep.Duration = testStep.End - testStep.Start;
            testStep.Status = TestStepStatus.Completed;

            this.DoProcess(0, "Completed");
            this.WriteTestStep(testStep);
            return testStep;
        }

        public Step Mix1()
        {
            Step testStep = new Step();
            testStep.Description = "Mixed1. Insert, update, load simultaneously";

            Step insertStep, updateStep;
            Step[] loadSteps;

            ParallelOptions options = new ParallelOptions();
            options.MaxDegreeOfParallelism = 99;

            Parallel.Invoke(options,
                () => { insertStep = this.TestMultithreadedInsert(); },
                () => { updateStep = this.TestMultithreadedUpdate(); },
                () => { loadSteps = this.TestLoad(true); });

            testStep.RecordCount = TestManager<T>.DefaultSampleSize;
            testStep.End = DateTime.Now;
            testStep.Duration = testStep.End - testStep.Start;
            testStep.Status = TestStepStatus.Completed;

            this.DoProcess(0, "Completed");
            this.WriteTestStep(testStep);
            return testStep;
        }

        public void Test()
        {
            Test test = new Test();
            test.Connector = this.CurrentConnection.Name;
            test.Start = DateTime.Now;

            test.Steps.Add(this.TestInsert());
            test.Steps.Add(this.TestUpdate());
            test.Steps.AddRange(this.TestLoad(false));
            test.Steps.Add(this.TestDelete());
            test.Steps.Add(this.TestInsertWithTransaction());
            test.Steps.Add(this.TestUpdateWithTransaction());
            test.Steps.Add(this.TestRollback());
            test.Steps.Add(this.TestBulkInsert());
            test.Steps.Add(this.TestMultithreadedInsert());
            test.Steps.Add(this.TestMultithreadedUpdate());
            test.Steps.AddRange(this.TestLoad(true));
            test.Steps.Add(this.Mix1());

            test.End = DateTime.Now;
            test.Duration = test.End - test.Start;
        }
    }
}
